﻿namespace NServiceBus.Features;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Logging;
using Microsoft.Extensions.DependencyInjection;
using Unicast;
using Unicast.Queuing;

/// <summary>
/// Used to configure auto subscriptions.
/// </summary>
public sealed class AutoSubscribe : Feature
{
    /// <summary>
    /// Creates a new instance of the auto subscribe feature.
    /// </summary>
    public AutoSubscribe() => Prerequisite(context => !context.Settings.GetOrDefault<bool>("Endpoint.SendOnly"), "Send only endpoints can't autosubscribe.");

    /// <summary>
    /// See <see cref="Feature.Setup" />.
    /// </summary>
    protected override void Setup(FeatureConfigurationContext context)
    {
        if (!context.Settings.TryGet(out SubscribeSettings settings))
        {
            settings = new SubscribeSettings();
        }

        var conventions = context.Settings.Get<Conventions>();

        context.RegisterStartupTask(b =>
        {
            var handlerRegistry = b.GetRequiredService<MessageHandlerRegistry>();

            return new ApplySubscriptions(handlerRegistry, conventions, settings);
        });
    }

    class ApplySubscriptions(
        MessageHandlerRegistry messageHandlerRegistry,
        Conventions conventions,
        SubscribeSettings subscribeSettings)
        : FeatureStartupTask
    {
        protected override async Task OnStart(IMessageSession session, CancellationToken cancellationToken = default)
        {
            var eventsToSubscribe = GetHandledEventTypes(messageHandlerRegistry, conventions, subscribeSettings);

            if (eventsToSubscribe.Length == 0)
            {
                Logger.Debug("Auto-subscribe found no event types to subscribe.");
                return;
            }

            try
            {
                var messageSession = session as MessageSession;
                await messageSession.SubscribeAll(eventsToSubscribe, new SubscribeOptions(), cancellationToken)
                    .ConfigureAwait(false);
                if (Logger.IsDebugEnabled)
                {
                    Logger.DebugFormat("Auto subscribed to events {0}",
                        string.Join<Type>(",", eventsToSubscribe));
                }
            }
            catch (AggregateException e)
            {
                foreach (var innerException in e.InnerExceptions)
                {
                    if (innerException is QueueNotFoundException)
                    {
                        throw innerException;
                    }

                    LogFailedSubscription(innerException);
                }
                //swallow
            }
            catch (QueueNotFoundException)
            {
                throw;
            }
            // also catch regular exceptions in case a transport does not throw an AggregateException
            catch (Exception ex) when (!ex.IsCausedBy(cancellationToken))
            {
                LogFailedSubscription(ex);
                // swallow exception
            }

            void LogFailedSubscription(Exception exception)
            {
                Logger.Error("AutoSubscribe was unable to subscribe to an event:", exception);
            }
        }

        protected override Task OnStop(IMessageSession session, CancellationToken cancellationToken = default) => Task.CompletedTask;

        static Type[] GetHandledEventTypes(MessageHandlerRegistry handlerRegistry, Conventions conventions, SubscribeSettings settings) =>
            [.. handlerRegistry.GetMessageTypes() //get all potential messages
                .GetHandledEventTypes(conventions)
                .Where(t => settings.AutoSubscribeSagas || handlerRegistry.GetHandlersFor(t).Any(handler => !typeof(Saga).IsAssignableFrom(handler.HandlerType))) //get messages with other handlers than sagas if needed
                .Except(settings.ExcludedTypes)];

        static readonly ILog Logger = LogManager.GetLogger<AutoSubscribe>();
    }

    internal class SubscribeSettings
    {
        public bool AutoSubscribeSagas { get; set; } = true;

        public HashSet<Type> ExcludedTypes { get; set; } = [];
    }
}